const express = require("express")
const logger = require("morgan")
const { StatusCodes } = require("http-status-codes")
const cors = require("cors")
// fs扩展
const fs = require("fs-extra")
const path = require("path")
const { pipeStream } = require("./utils")
const { CHUNK_SIZE } = require("./constant")
// 定时任务
const cron = require("node-cron")
const crypto = require('crypto')
const app = express()
const PUBLIC_DIR = path.resolve(__dirname, "public")
const TEMP_DIR = path.resolve(__dirname, "temp")

// 确保public目录存在
fs.ensureDirSync(PUBLIC_DIR)
// 临时目录，存放分片文件
fs.ensureDirSync(TEMP_DIR)

// 日志库
app.use(logger("dev"))
app.use(cors())
// 解析json
app.use(express.json())
// 表单
app.use(express.urlencoded({ extended: true }))

// 静态目录public，存放最终合并之后的文件
app.use(express.static(path.resolve(__dirname, "public")))

//每天（00:00）清理temp文件夹的定时任务,清理的是创建时间为一周前的分片文件夹
/**
 * 1.分钟 (0-59)：表示在每小时的第几分钟执行任务。
 * 2.小时 (0-23)：表示在每天的第几个小时执行任务。
 * 3. 日 (1-31)：表示在每月的第几天执行任务。
 * 4. 月 (1-12)：表示在每年的第几个月执行任务。
 * 5. 星期 (0-7)：表示在每周的第几天执行任务，其中 0 和 7 都表示星期天。
 *  
 * 特殊字符
 * *：表示任何值。例如，* 在分钟字段中表示每分钟。
 * ,：表示列出多个值。例如，1,2,3 表示在第 1、2 和 3 分钟执行。
 * -：表示范围。例如，1-5 表示从第 1 到第 5 分钟。
 * /：表示步长。例如，'* / 5' 表示每 5 分钟执行一次。
 */
cron.schedule('0 0 * * *', async () => {
  try {
    const directories = await fs.readdir(TEMP_DIR);
    const now = Date.now();
    const oneWeekAgo = now - (7 * 24 * 60 * 60 * 1000); // 一周前的时间戳

    for (const dir of directories) {
      const dirPath = path.resolve(TEMP_DIR, dir);
      const stats = await fs.stat(dirPath);
      if (stats.isDirectory() && stats.birthtimeMs < oneWeekAgo) {
        // 如果是目录且创建时间在一周之前，删除该目录
        await fs.rmdir(dirPath, { recursive: true });
        console.log(`Deleted old temp directory: ${dirPath}`);
      }
    }
  } catch (err) {
    console.error('Error clearing old temp directories:', err);
  }
});

// 上传分片
app.post("/upload/:fileName", async (req, res, next) => {
  const { fileName } = req.params
  // 对应的是请求时候的params参数对象
  let { chunkFileName, start } = req.query
  // 如果start是NaN，则设置为0
  start = isNaN(start) ? 0 : parseInt(start, 10)
  // 文件的分片所属的目录（名字是fileName)
  const chunkDir = path.resolve(TEMP_DIR, fileName)
  // 分片存放的目录(名字是fileName-index)
  const chunkFilePath = path.resolve(chunkDir, chunkFileName)
  // 确保分片目录存在(异步)
  await fs.ensureDir(chunkDir)
  // 创建写入流,设置start字节偏移量和flags追加写入,如果文件不存在则创建文件
  const ws = fs.createWriteStream(chunkFilePath, { start, flags: 'a' })
  // 监听请求中断事件(请求到一半，页面刷新了，或者是axios的请求中断), 中断时关闭写入流
  req.on('aborted', () => ws.close())
  try {
    // 管道方式，将请求体流数据写入，req请求体和ws可写流
    await pipeStream(req, ws)
    res.json({
      code: StatusCodes.OK,
      message: "上传成功"
    })
  } catch (e) {
    next(e)
  }
})

// 合并分片
app.get("/merge/:fileName", async (req, res, next) => {
  const { fileName } = req.params

  try {
    await mergeChunks(fileName)
    const filePath = path.resolve(PUBLIC_DIR, fileName)
    const hash = crypto.createHash('sha256')
    const fileBuffer = await fs.readFile(filePath)
    hash.update(fileBuffer)
    const fileHash = hash.digest('hex')
    if (fileName.split('.')[0] === fileHash) {
      res.json({
        code: StatusCodes.OK
      })
    } else {
      await fs.rm(filePath)
      res.json({
        code: StatusCodes.INTERNAL_SERVER_ERROR
      })
    }
  } catch (e) {
    next(e)
  }

  async function mergeChunks(fileName) {
    // fileName目录
    const chunkDir = path.resolve(TEMP_DIR, fileName)
    // 获取fileName目录下的chunkFileName文件名的数组
    const chunkFileNames = await fs.readdir(chunkDir)
    // 文件的顺序不能保证，先将文件名按照index进行排序，方便后面读写
    chunkFileNames.sort((a, b) => Number(a.split("-")[1]) - Number(b.split("-")[1]))
    try {
      const pipes = chunkFileNames.map((chunkFileName, index) => {
        // 可读流读取，自动关闭
        // 可写流的字节偏移量start，每次读取写入对应位置
        return pipeStream(
          fs.createReadStream(path.resolve(chunkDir, chunkFileName), { autoClose: true }),
          fs.createWriteStream(path.resolve(PUBLIC_DIR, fileName), {
            start: index * CHUNK_SIZE
          })
        )
      })
      // 等待所有管道流完成
      await Promise.all(pipes)
      // 删除temp中的fileName的临时目录,递归删除操作,删除目录下的所有子文件
      await fs.rm(chunkDir, { recursive: true })
    } catch (e) {
      next(e)
    }
  }
})

app.get('/verify/:fileName', async (req, res, next) => {
  const { fileName } = req.params
  // 获取文件在public下面的目录
  const filePath = path.resolve(PUBLIC_DIR, fileName)
  // 判断文件是否存在
  const isExist = await fs.pathExists(filePath)
  if (isExist) {
    // 文件存在，不需要上传
    return res.json({
      code: StatusCodes.OK,
      needUpload: false
    })
  }
  // 获取temp中该fileName对应的路径
  const chunkDir = path.resolve(TEMP_DIR, fileName)
  // 判断该路径是否存在
  const isExistChunkDir = await fs.pathExists(chunkDir)
  // 如果存在，说明文件已经上传了一部分，需要断点续传
  let uploadedChunksList = []
  if (isExistChunkDir) {
    const chunkFileNames = await fs.readdir(chunkDir)
    uploadedChunksList = await Promise.all(chunkFileNames.map(async (chunkFileName) => {
      const { size } = await fs.stat(path.resolve(chunkDir, chunkFileName))
      return { chunkFileName, size }
    }))
  }
  res.json({
    code: StatusCodes.OK,
    needUpload: true,
    uploadedChunksList
  })
})
app.listen(8080, () => {
  console.log("server is running on port 8080")
})
